﻿using System;
using System.Text;
using AcademicPlanner.Domain.Models;
using AcademicPlanner.Framework.Libraries.Auth;
using AcademicPlanner.Framework.Helpers;

namespace AcademicPlanner.Domain.Components
{
	/// <summary>
	/// List of names of access control operations.
	/// </summary>
	public enum ACOperationName
	{
		CREATE_PERSON,
		CREATE_ACCOUNT
	}

	/// <summary>
	/// List of purposes of acess control object tokens.
	/// </summary>
	public enum ACObjectTokenPurpose
	{
		AUTH
	}

	/// <summary>
	/// AuthMngr is the component of the AcademicPlanner application
	/// responsible for access control. AuthMngr uses <see cref="T:AcademicPlanner.Framework.Libraries.Auth.DBorOpenIdBAuthenticator"/>
	/// for authentication and <see cref="T:T:AcademicPlanner.Framework.Libraries.Auth.RnRBAuthorizer"/>
	/// for authorization.
	/// </summary>
    public class AuthMngr : Component
    {
		private IAuthenticator authn;

		private IAuthorizer authz;

		private AuthenticationResult authnResult;

		private readonly string guestUserName = "sys_guest";

		private readonly string guestUserPassword = "";

		private readonly string objectTokenHashSalt = ";;objecttoken;;";

		public static readonly string SYS_USER_NAMES_PREFIX = "sys_";


		private AuthMngr() 
		{
			authn = new DBorOpenIdBAuthenticator(DomainMaster.DSAS.Connection.ConnectionString);
			authz = new RnRBAuthorizer(DomainMaster.DSAS.Connection.ConnectionString);
		}

		private class InstanceHolder 
		{
			internal static AuthMngr INSTANCE = new AuthMngr(); 
		}


		/// <summary>
		/// Gets the single AuthMngr intance.
		/// </summary>
		/// <returns>The single AuthMngr intance.</returns>
		/// <version>1.0</version>
		/// <since>1.0</since>
		public static AuthMngr GetInstance()
		{ 
			return InstanceHolder.INSTANCE; 
		}

		/// <summary>
		/// Returns whether the current user in the session is allowed
		/// to perform a given access control operation.
		/// </summary>
		/// <param name="pOperation">The access control operation.</param>
		/// <returns>Whether the access control operation is allowed for the current session user.</returns>
		/// <seealso cref="M:IsUserAuthorized(ACOperationName, Model)"/>
		public bool IsUserAuthorized(ACOperationName pOperation)
		{
			return IsUserAuthorized(pOperation, null);
		}

		/// <summary>
		/// Returns whether the current user in the session is allowed
		/// to perform a given access control operation on a given <c>Model</c>.
		/// This method authenticates the session user first.
		/// </summary>
		/// <param name="pOperation">The access control operation.</param>
		/// <param name="pModel">The <c>Model</c> the operation is to be performed on.</param>
		/// <returns>Whether the access control operation is allowed for the current session user.</returns>
		/// <see cref="M:AuthenticateUser"/>
		public bool IsUserAuthorized(ACOperationName pOperation, Model pModel)
		{
			AuthorizationResult vRes;
			var vOp = pOperation.ToString();

			TryAuthenticateUser();
			if (authnResult.IsAuthentic)
			{
				var vUser = authnResult.UserIdentifier;

				if (pModel == null)
					vRes = authz.GetAuthorization(vUser, vOp);
				else
					vRes = authz.GetAuthorization(vUser, vOp, GetModelACObjectIdentifier(pModel));
				//throw new Exception(((RnRBAuthorizationResult)vRes).Reason.ToString());
				return vRes.IsAuthorized;
			}

			return false;
		}

		/// <summary>
		/// Authenticates the session user. If the session user's identifier is
		/// null or empty, he is assumed to be a guest.
		/// </summary>
		/// <returns>Whether the session user is authentic or not.</returns>
		/// <seealso cref="M:AcademicPlanner.Framework.Helpers.SessionHelper.GetUserName"/>
		/// <seealso cref="M:AcademicPlanner.Framework.Helpers.SessionHelper.GetUserPassword"/>
		public bool AuthenticateUser()
		{
			var vUser = SessionHelper.GetUserName();
			var vPswd = SessionHelper.GetUserPassword();

			if (string.IsNullOrEmpty(vUser))
			{
				vUser = guestUserName;
				vPswd = guestUserPassword;
			}
			authnResult = authn.Authenticate(vUser, vPswd);

			return authnResult.IsAuthentic;
		}

		private void TryAuthenticateUser()
		{
			if (authnResult.UserIdentifier == null)
				AuthenticateUser();
		}

		private string GetModelACObjectIdentifier(Model pModel)
		{
			return GetModelACObjectIdentifier(pModel.GetType(), pModel.EntityId);
		}

		private string GetModelACObjectIdentifier(Type pModelType, int pModelId)
		{
			return string.Format("{0}-{1}", pModelType.Name, pModelId);
		}

		/// <summary>
		/// Gets or creates an authentication token for a <c>Model</c>.
		/// </summary>
		/// <param name="pModel">The <c>Model</c>.</param>
		/// <returns>The authentication token.</returns>
		/// <exception cref="T:AcademicPlanner.Domain.CriticalDomainException">
		/// Thrown with error code <c>DomainErrorCode.C_ADD_ACOBJECT_TOKEN_FAILURE</c> 
		/// when an error occured while persisting the newly created token.
		/// </exception>
		public string GetAuthenticationToken(Model pModel)
		{
			var vObject = GetModelACObjectIdentifier(pModel);
			var vToken = ((RnRBAuthorizer)authz).GetNotExpiredObjectToken(vObject, 
																			ACObjectTokenPurpose.AUTH.ToString());

			if (vToken == null)
			{
				vToken = SecurityHelper.Hash(DateTime.Now.ToLongDateString(),
											 new StringBuilder(vObject).Append(objectTokenHashSalt)
																		.Append(ACObjectTokenPurpose.AUTH)
																		.ToString());
				if (!((RnRBAuthorizer)authz).AddObjectToken(vObject, ACObjectTokenPurpose.AUTH.ToString(), vToken))
					throw new CriticalDomainException(DomainErrorCode.C_ADD_ACOBJECT_TOKEN_FAILURE);
			}

			return vToken;
		}

		/// <summary>
		/// Returns whether an authentication token is valid for a given <c>Model</c>.
		/// </summary>
		/// <param name="pToken">The authentication token.</param>
		/// <param name="pModelType">The <c>System.Type</c> of the <c>Model</c>.</param>
		/// <param name="pModelId">The <c>Model</c> identifier.</param>
		/// <returns>Whether the authentication token is valid or not.</returns>
		public bool IsAuthenticationTokenValid(string pToken, Type pModelType, int pModelId)
		{
			var vObject = GetModelACObjectIdentifier(pModelType, pModelId);
			var vToken = ((RnRBAuthorizer)authz).GetNotExpiredObjectToken(vObject, 
																				ACObjectTokenPurpose.AUTH.ToString());
			return (vToken != null && vToken.Equals(pToken));
		}

		/// <summary>
		/// Discards the authentication token of a <c>Model</c> and renders it
		/// invalid for future use.
		/// </summary>
		/// <param name="pModel"></param>
		public void DiscardAuthenticationToken(Model pModel)
		{
			var vObject = GetModelACObjectIdentifier(pModel);
			((RnRBAuthorizer)authz).ExpireObjectToken(vObject, ACObjectTokenPurpose.AUTH.ToString());
		}

		/// <summary>
		/// Registers a <c>Model</c> into the access control sub system.
		/// </summary>
		/// <param name="pModel">The <c>Model</c> to register.</param>
		/// <param name="pUser">The <c>User</c> "owning" the <c>Model</c>.</param>
		public void RegisterIntoACSystem(Model pModel, User pUser)
		{
			var vObject = GetModelACObjectIdentifier(pModel);

			if (!((RnRBAuthorizer)authz).AddObject(vObject, pUser.EntityId))
				throw new CriticalDomainException(DomainErrorCode.C_ADD_ACOBJECT_FAILURE);
		}

		/// <summary>
		/// Returns whether a username is allowed as such for a <c>User</c>.
		/// <c>User</c>s usernames are not allowed to start with <see cref="P:SYS_USER_NAMES_PREFIX"/>.
		/// </summary>
		/// <param name="pUserName">The username to check.</param>
		/// <returns>Whether the username is allowed.</returns>
		public bool IsUserNameAllowed(string pUserName)
		{
			return !pUserName.StartsWith(SYS_USER_NAMES_PREFIX);
		}
    }
}
